Flutter |
您所在的位置:网站首页 › flutter tabview 状态 › Flutter |
NestedScrollView
CustomScrollView只能组合Sliver,如果有子组件也是一个可滚动(通过SliverToBoxAdapter嵌入)且它们的滑动方向一致时便不能工作。为了解决这个问题,Flutter提供了NestedScrollView组件,它的功能是协调两个可滚动组件。 const NestedScrollView({ ... //省略可滚动组件的通用属性 //header,sliver构造器 required this.headerSliverBuilder, //可以接受任意的可滚动组件 required this.body, this.floatHeaderSlivers = false, })上面效果有三个部分组成: 最上面的一个AppBar,实现导航,需要固定在顶部。 AppBar下面是一个SliverList,可以有任意多个列表项 最下面的ListView。预期效果是SliverList和下面的ListView的滑动能够统一,而不是下面ListView上滑动时只有ListView响应滑动,整个页面在垂直方向是一个整体。 Material( child: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { // 返回一个 Sliver 数组给外部可滚动组件。 return [ SliverAppBar( title: const Text('嵌套ListView'), pinned: true, // 固定在顶部 forceElevated: innerBoxIsScrolled, ), buildSliverList(5), //构建一个 sliverList ]; }, body: ListView.builder( padding: const EdgeInsets.all(8), physics: const ClampingScrollPhysics(), //重要 itemCount: 30, itemBuilder: (BuildContext context, int index) { return SizedBox( height: 50, child: Center(child: Text('Item $index')), ); }, ), ), );NestedScrollView在逻辑上将可滚动组件分为了header和body两部分,header部分可以认为外部可滚动组件(outer scroll view),可以认为这个可滚动组件就是CustomScrollView,所以它只能接收Sliver,通过headerSliverBuilder来构建一个Sliver列表给外部的可滚动组件;而body部分可以接收任意的可滚动组件,该可滚动组件称为内部可滚动组件(inner scroll view)。 NestedScrollView原理综上: 要确认内部的可滚动组件(body)的physics是否需要设置为ClampingScrollPhysics。比如,当ListView没有设置为ClampingScrollPhysics,则用户快速滑动到顶部时,会执行一个弹性效果,此时ListView就会与header显得割裂(滑动效果不统一)。所以需要设置。但是如果header中只有一个SliverAppBar则不应该加,因为SliverAppBar是固定在顶部的,ListView滑动到顶部时上面已经没有要继续往下滑动的元素来,所以此时出现弹性效果是符合预期的。 内部的可滚动组件(body)不能设置controller和primary,这是因为NestedScrollView的协调器中已经指定来它的Controller,如果重新设定则协调器会失效。 SliverAppBarSliverAppBar是AppBar的Sliver版,但多数的参数相同,但是SliverAppBar有一些特有的功能: const SliverAppBar({ this.collapsedHeight, // 收缩起来的高度 this.expandedHeight,// 展开时的高度 this.pinned = false, // 是否固定 this.floating = false, //是否漂浮 this.snap = false, // 当漂浮时,此参数才有效 bool forceElevated //导航栏下面是否一直显示阴影 ... }) SliverAppBar在NestedScrollView中随着用户的滑动可以收缩和展开,因此需要分别指定收缩和展开时的高度。 pinned为True时SliverAppBar会固定在NestedScrollView的顶部,行为和SliverPersistentHeader的pinned功能一致。 floating和snap:floating为true时,SliverAppbar不会固定到顶部,当用户向上滑动到顶部时,SliverAppbar也会滑出可视窗口。当用户反向滑动时,SliverAppBar的snap为true时,此时无论SliverAppbar已经滑出屏幕多远,都会立即回到屏幕顶部;但是snap为false,则SliverAppBar只有当向下滑到边界时才会重新回到屏幕顶部。这一点和SliverPersistentHeader的floating相似,但不同的是SliverPersistentHeader没有snap参数,当它的floating为true时,效果时等同于SliverAppBar的floating和snap同时为true时效果。SliverAppBar的一些参数和SliverPersistentHeader很像,因为SliverAppBar内部包含一个SliverPersistentHeader,用于实现顶部固定和漂浮效果。 class SnapAppBar extends StatelessWidget { const SnapAppBar({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ //如果此处不使用SliverOverlapAbsorber头部会有部分列表被遮挡 SliverOverlapAbsorber( //传递重叠长度 handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( floating: true, snap: true, expandedHeight: 200, flexibleSpace: FlexibleSpaceBar( background: Image.asset( "./imgs/sea.png", fit: BoxFit.cover, ), ), forceElevated: innerBoxIsScrolled, ), ), ]; }, body: Builder(builder: (BuildContext context) { return CustomScrollView( slivers: [ SliverOverlapInjector( //传递重叠长度 handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), buildSliverList(100) ], ); }), ), ); } }注意: SliverAppBar用SliverOverlapAbsorber包裹起来的作用是,获取SliverAppbar返回是遮住内部可滚动组件的部分长度,这个长度就是overlap(重叠)的长度。 在body中往CustomScrollView的Sliver列表的最前面插入了一个SliverOverlapInjector,它会将SliverOVerlapAbsorber中获取的overlap长度应用到内部可滚动组件中。这样在SliverAppBar返回时,内部可滚动组件也会相应的同步滑动相应的距离。SliverOverlapAbsorber和SliverOverlapInjector都接收一个handle,给它传入的是NestedScrollView.sliverOverlapAbsorberHandleFor(context)。handle就是SliverOverlapAbsorber和SliverOverlapInjector的通信桥梁,即传递overlap长度。 当snap为true时,只需要给SliverAppBar包裹一个SliverOverlapAbsorber即可,而无需再给CustomScrollView添加SliverOverlapinjector,因为这种情况SliverOverlapAbsorber会自动吸收Overlap,以调整自身的布局高度为SliverAppBar的实际高度,这样的话header的高度变化后就会自动将body向下撑(header和body属于同一个CustomScrollView),同时handle中的overlap长度始终0。而只有当SliverAppBar被SliverOverlapAbsorber包裹且为固定模式时(pinned为true),CustomScrollView中添加SliverOverlapInjector才有意义,handle中的overlap长度不为0。 验证: class SnapAppBar2 extends StatefulWidget { const SnapAppBar2({Key? key}) : super(key: key); @override State createState() => _SnapAppBar2State(); } class _SnapAppBar2State extends State { // 将handle 缓存 late SliverOverlapAbsorberHandle handle; void onOverlapChanged(){ // 打印 overlap length print(handle.layoutExtent); } @override Widget build(BuildContext context) { return Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { handle = NestedScrollView.sliverOverlapAbsorberHandleFor(context); //添加监听前先移除旧的 handle.removeListener(onOverlapChanged); //overlap长度发生变化时打印 handle.addListener(onOverlapChanged); return [ SliverOverlapAbsorber( handle: handle, sliver: SliverAppBar( floating: true, snap: true, // pinned: true, // 放开注释,然后看日志 expandedHeight: 200, flexibleSpace: FlexibleSpaceBar( background: Image.asset( "./imgs/sea.png", fit: BoxFit.cover, ), ), forceElevated: innerBoxIsScrolled, ), ), ]; }, body: LayoutBuilder(builder: (BuildContext context,cons) { return CustomScrollView( slivers: [ SliverOverlapInjector(handle: handle), buildSliverList(100) ], ); }), ), ); } @override void dispose() { // 移除监听器 handle.removeListener(onOverlapChanged); super.dispose(); } }分别查看snap和pinned模式下控制台的输出即可验证。 综上:建议SLiverOverlapAbsorber和SliverOverlapInjector配对使用,这样可以避免日后将snap模式改为固定模式后忘记添加SliverOverlapInjector而导致bug。 嵌套TabBarView使用实例: class NestedTabBarView1 extends StatelessWidget { const NestedTabBarView1({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final _tabs = ['猜你喜欢', '今日特价', '发现更多']; // 构建 tabBar return DefaultTabController( length: _tabs.length, // tab的数量. child: Scaffold( body: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), sliver: SliverAppBar( title: const Text('商城'), floating: true, snap: true, forceElevated: innerBoxIsScrolled, bottom: TabBar( tabs: _tabs.map((String name) => Tab(text: name)).toList(), ), ), ), ]; }, body: TabBarView( children: _tabs.map((String name) { return Builder( builder: (BuildContext context) { return CustomScrollView( key: PageStorageKey(name), slivers: [ SliverOverlapInjector( handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), ), SliverPadding( padding: const EdgeInsets.all(8.0), sliver: buildSliverList(50), ), ], ); }, ); }).toList(), ), ), ), ); } } |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |